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:
Not sure we can do something about it, as Tim said, "scikit-learn compatible estimators" needs to be clonable.

Maybe you can try to explain your use case a bit more? In particular can you give more details about your motivation behind having a `threading.Event` in your xgboost callback?

Generic advice for a possible work-arounds, I would try to see whether custom pickling could help here, e.g. implementing `__getstate__`/`__setstate__` or `__reduce__`. Maybe implementing custom cloning through `__sklearn_clone__` could help as well.

Labels: Needs Investigation