MultiOutputRegressor can't process estimators with synchronization primitives

### Describe the bug

[MultiOutputRegressor ](https://scikit-learn.org/stable/modules/generated/sklearn.multioutput.MultiOutputRegressor.html) can't process estimators with threading/multiprocessing synchronization primitives

I want to propagate stop_event to the callback of regressor. I think the issue is because MultiOutputRegressor is trying to pickle each of estimator to move it to another thread/process. And if the estimator contains any synchronization primitives - they can't be pickled, so it fails. Maybe the solution might be to allow to provide pre-created estimators (for each of output) and provide them to the init of MultiOutputRegressor.

I need to use MultiOutputRegressor because I need to export XGBoost model into onnx with a help of [skl2onnx](https://onnx.ai/sklearn-onnx/). If I don't use MultiOutputRegressor  - skl2onnx doesn't allow me to export, despite XGBoost has an [experimental way of multiple outputs](https://xgboost.readthedocs.io/en/stable/tutorials/multioutput.html).

Or maybe I missed something. Please help.


Packages:

```
xgboost                   3.0.0
```

### Steps/Code to Reproduce

```python
from threading import Event

from sklearn.model_selection import train_test_split
from sklearn.datasets import make_regression
from sklearn.multioutput import MultiOutputRegressor

from xgboost import XGBRegressor, Booster
from xgboost import callback as xgb_callbacks


class Callback(xgb_callbacks.TrainingCallback):
    def __init__(self, stop_event: Event):
        super().__init__()
        self.stop_event = stop_event

    def after_iteration(self, model: Booster, epoch: int, evals_log: dict[str, dict]) -> bool:
        print(f"xgboost training: epoch {epoch}, evals_log {evals_log}")
        return False


def train_xgboost(X_train, y_train):
    stop_event = Event()

    base_model = XGBRegressor(n_estimators=45, callbacks=[Callback(stop_event)])
    model = MultiOutputRegressor(base_model)
    # base_model.callbacks = [Callback(stop_event)]

    eval_set = []
    for i in range(y_train.shape[1]):
        y = y_train[:, i]
        eval_set.append((X_train, y))

    model.fit(X=X_train, y=y_train, eval_set=eval_set, verbose=True)

    return model


def get_data():
    X, y = make_regression(n_samples=100,
                           n_features=5,
                           n_informative=2,
                           n_targets=3,
                           noise=10.0,
                           random_state=42)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
    return X_train, y_train, X_test, y_test

def main():
    X_train, y_train, X_test, y_test = get_data()
    ml_model = train_xgboost(X_train, y_train)


if __name__ == "__main__":
    main()
```

### Expected Results

No errors

### Actual Results

```
Traceback (most recent call last):
  File "D:\my_test\issue_pickl.py", line 54, in <module>
    main()
  File "D:\my_test\issue_pickl.py", line 50, in main
    ml_model = train_xgboost(X_train, y_train)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\issue_pickl.py", line 33, in train_xgboost
    model.fit(X=X_train, y=y_train, eval_set=eval_set, verbose=True)
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 1389, in wrapper
    return fit_method(estimator, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\multioutput.py", line 274, in fit
    self.estimators_ = Parallel(n_jobs=self.n_jobs)(
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\utils\parallel.py", line 77, in __call__
    return super().__call__(iterable_with_config)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\joblib\parallel.py", line 1918, in __call__
    return output if self.return_generator else list(output)
                                                ^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\joblib\parallel.py", line 1847, in _get_sequential_output
    res = func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\utils\parallel.py", line 139, in __call__
    return self.function(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\multioutput.py", line 59, in _fit_estimator
    estimator = clone(estimator)
                ^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 94, in clone
    return estimator.__sklearn_clone__()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 300, in __sklearn_clone__
    return _clone_parametrized(self)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 127, in _clone_parametrized
    new_object_params[name] = clone(param, safe=False)
                              ^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 95, in clone
    return _clone_parametrized(estimator, safe=safe)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 105, in _clone_parametrized
    return estimator_type([clone(e, safe=safe) for e in estimator])
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 105, in <listcomp>
    return estimator_type([clone(e, safe=safe) for e in estimator])
                           ^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 95, in clone
    return _clone_parametrized(estimator, safe=safe)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\site-packages\sklearn\base.py", line 108, in _clone_parametrized
    return copy.deepcopy(estimator)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 146, in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 146, in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 172, in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 271, in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 146, in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 231, in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
  File "D:\my_test\sys\win64\python\Lib\copy.py", line 161, in deepcopy
    rv = reductor(4)
         ^^^^^^^^^^^
TypeError: cannot pickle '_thread.lock' object
```

### Versions

```shell
System:
    python: 3.11.9 (tags/v3.11.9:de54cf5, Apr  2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)]
executable: D:\my_test\sys\win64\python\python.exe
   machine: Windows-10-10.0.22631-SP0

Python dependencies:
      sklearn: 1.6.1
          pip: 25.0.1
   setuptools: 65.5.0
        numpy: 1.26.4
        scipy: 1.15.2
       Cython: None
       pandas: 2.2.3
   matplotlib: 3.10.1
       joblib: 1.4.2
threadpoolctl: 3.6.0

Built with OpenMP: True

threadpoolctl info:
       user_api: openmp
   internal_api: openmp
    num_threads: 32
       filepath: D:\my_test\sys\win64\python\Lib\site-packages\sklearn\.libs\vcomp140.dll
        version: None

       user_api: blas
   internal_api: openblas
    num_threads: 24
         prefix: libopenblas
       filepath: D:\my_test\sys\win64\python\Lib\site-packages\numpy.libs\libopenblas64__v0.3.23-293-gc2f4bdbb-gcc_10_3_0-2bde3a66a51006b2b53eb373ff767a3f.dll
        version: 0.3.23.dev
threading_layer: pthreads
   architecture: Prescott

       user_api: blas
   internal_api: openblas
    num_threads: 24
         prefix: libscipy_openblas
       filepath: D:\my_test\sys\win64\python\Lib\site-packages\scipy.libs\libscipy_openblas-f07f5a5d207a3a47104dca54d6d0c86a.dll
        version: 0.3.28
threading_layer: pthreads
   architecture: Haswell
```

COMMENT:
Thanks for opening this. One assumption about "scikit-learn compatible estimators" is that they are `clone()`able. This eventually ends up calling `copy.deepcopy()` which uses pickle to try and make a copy of a `Event`. And it all falls apart.

I'm not sure what we could (easily) do for this. While you could maybe work around it in this specific case, the more fundamental problem that the estimator you are using isn't `clone`able would remain. You'd possible run into the same problem somewhere else/scikit-learn would grow a bunch of locations where this works and others where it doesn't.

Labels: Needs Investigation